#!/usr/bin/python

import sys, os, cmd, datetime, shlex,re
from getopt import getopt
from os.path import expanduser
import readline
import readline, imp

opts,args = getopt(sys.argv[1:],'h',['help'])
opts=dict(opts)
if '-h' in opts or '--help' in opts:
    print """ Usage %s [-h | --help] [<conf_path>]
    <conf_path> default to ~/.clipf/
    """ % sys.argv[0]
    sys.exit(0)
confpath=expanduser(args and args[0] or '~/.clipf/')
if not os.access(confpath,os.F_OK):
    print "config don't exists, creating...",
    os.mkdir(confpath)
    dbpath=confpath+'db/'
    os.mkdir(dbpath)
    open(dbpath+'op','w').close()
    open(dbpath+'prod','w').close()

    cf=open(confpath+'clipf.conf','w')
    cf.write("""
# clim configuration

acc='00'
max_lines=40
prompt="\033[31m%(date)s:%(acc)s> \033[0m"

fmts={
    'prod':'\033[34;1m%%(%s)-10.10s\033[0m',
    'prod_code':'\033[34;1m%%(%s)-10.10s\033[0m',
    'prod_code_full':'\033[34;1m%%(%s)s\033[0m',
    'acc_id':'\033[35;1m%%(%s)-10.10s\033[0m',
    'prod_name':'\033[36m%%(%s)-16s\033[0m',
    'note':'\033[36m%%(%s)-16s\033[0m',
    'amount':'\033[37m%%(%s)8.2f\033[0m',
    'date':'\033[0m%%(%s)s',
    'header':'\033[34;1m',
    'param':'\033[35;1m%%(%s)s\033[0m',
    }
aliases={
        'oo ': 'op add ',
        'ol': 'op ls',
        'pl': 'prod ls'
        'o ': 'op '
        'p ': 'prod ',
        'r ': 'rep '
        }
    """)
    cf.close()
    print "Done"
config = imp.load_source('config',confpath+'clipf.conf')


spln='\033[34m--------------------------------------\033[0m'

class myerr(Exception): pass

def check(cond, msg):
    if not cond:
        raise myerr(msg)

class App(cmd.Cmd):
    prompt="$>"
    def __init__(self):
        cmd.Cmd.__init__(self)
        dt=datetime.date.today().isoformat()
        self.opt={
                'date': dt,
                'date_from':dt,
                'date_to':dt,
                'acc': config.acc,
                'max_lines': config.max_lines
                }
        # load prod list
        self.db=confpath+'db/'
        self.prodfmt="%(prod)s:%(dc)d:%(name)s\n"
        self.opfmt="%(date)s:%(acc)s:%(prod)s:%(amount).2f:%(dc)d:%(note)s\n"
        flds=('prod','dc','name')
        f=open(self.db+'prod','r')
        prods=[]
        for ln in f:
            r=dict(zip(flds,ln.rstrip('\n').split(':')))
            r['dc']=int(r['dc'])
            grp=r['prod'].rstrip('.')
            try:
                pp=grp.rindex('.')
                grp=grp[:pp+1]
            except ValueError:
                grp=''
            r['grp']=grp
            prods.append(r)
        f.close()
        self.prods=prods
        self.prodd=dict([(p['prod'],p) for p in prods])
        self.fmts=config.fmts
        self.aliases=config.aliases
        self.setPrompt()
    def setPrompt(self):
        self.prompt=config.prompt % self.opt
    def precmd(self,line):
        for key in self.aliases.iterkeys():
            if line.startswith(key):
                line=self.aliases[key]+line[len(key):]
        return line
    def preloop(self):
        print """Personal finance accounting in command line.
Type "help" for list of available commands. Type "help <command>" for help about particular command."""
    def _fx(self,m):
        m=m.group(1)
        if ':' in m:
            fn,dt = m.split(':')
        else:
            dt,fn=m,m
        return self.fmts[dt] % fn
    def ffmt(self,fmt):
        pat=re.compile(r"\$\(([a-z_:]+)\)")
        return re.sub(pat,self._fx,fmt).replace('[[',self.fmts['header']).replace(']]','\033[0m')
    def gen_completions(self,text):
        s=readline.get_line_buffer()
        grp=s and (s[-1]!=' ') and s.split()[-1] or ''
        pp=grp.rfind('.')
        if pp==-1:
            prod_group=''
        else:
            prod_group=grp[:pp+1]
        data=[r['prod'][len(r['grp']):] for r in self.prodd.itervalues() 
                if r['prod'].startswith(grp) and r['grp']==prod_group]
        data.sort()
        return data

    def complete(self,text,state):
        if state==0:
            self.completions=self.gen_completions(text)+[None]
        return self.completions[state]
    def select(self):
        flds=('date','acc','prod','amount','dc','note')
        f=open(self.db+'op','r')
        for ln in f:
            r=dict(zip(flds,ln.rstrip('\n').split(':')))
            r['amount']=float(r['amount'])
            r['dc']=int(r['dc'])
            yield r
        f.close()
    def dest(self,lst):
        if len(lst)>int(self.opt['max_lines']) and self.interactive:
            return os.popen('less','w')
        else:
            return None
    def _cmd(self,cmd,c,usage):
        try:
            cl=shlex.split(c)
            check(len(cl),usage)
            cc=cl[0]
            meth=getattr(self,'_%s_%s' % (cmd, cc))
            meth(cl[1:])
        except AttributeError:
            print usage
        except myerr, e:
            print e
        return 0

    def do_prod(self,c):
        self._cmd('prod',c,"Usage: prod ( ls | add | rm ) <args>")
    def help_prod(self):
        print """ Usage: prod <subcommand> [options] [<args>] 
    Subcommands:
        add [-d] <item_code> <item_name> 
            - add new item. '-d' option mark item as income
              Type <item_name> in quotes, if it contain spaces
        rm <item_code_pattern>
            - remove all items, which code starts with <item_code_pattern>
        ls [<item_code>]
            - list all items in <item_code> group
          """
    def do_op(self,c):
        self._cmd('op',c,"Usage: op ( ls | add ) <args>")
    def help_op(self):
        print """ Usage: op <subcommand> [<options>] <args>
    Subcommands:
        add [-d <date>] [-a <account>] <item_code> <amount> [<note>]
            - add new operation.
                '-d' - override operation date to <date> (see 'help set' for details)
                '-a' - override operation account to <account> (see 'set' for details)
              the income/expense flag for operation would be taken from item 
              type <note> in quotes, if it contain spaces
        ls [<item_code_pattern>]
            - show list of operations for the default reporting period (see 'help set' for details)
              if <item_code_pattern> specified, show only operations with items, which code starts with pattern
    Note:
        There is no delete command for operation. You need to add the same operation with negative amount
        to revoke operation.
            """

    def do_rep(self,c):
        self._cmd('rep',c,"Usage: rep ( prod | acc )")
    def help_rep(self):
        print """ Usage: rep <subcommand> [<args>]
    Subcommands:
        prod [<item_code>]
            - show turnover report with total turnover by each subling of <item_code> item group. 
              Default by root group.
        acc
            - show turnover and remains by each account
        """

    def _rep_prod(self,args):
        prod_group=args and args[0] or ''
        dt,ct = 0.0, 0.0
        dfrom,dto,acc = [self.opt[x] for x in ('date_from','date_to','acc')]
        grplen=len(prod_group)
        groups={}
        for r in self.select():
            if dfrom<=r['date']<=dto and r['acc']==acc and r['prod'].startswith(prod_group):
                prod=r['prod'][grplen:]
                pp=prod.find('.')
                if pp==-1:
                    grp=prod
                else:
                    grp=prod[:pp+1]
                rr=groups.get(grp,None)
                if not rr:
                    rr={'prod':grp, 'dt':0.0, 'ct':0.0}
                    groups[grp]=rr
                amount,dc=r['amount'],r['dc']
                rr['dt']+=amount*dc #income
                rr['ct']+=amount*(1-dc) #expense
        data=groups.values()
        data.sort(key=lambda rr:rr['prod'])
        dest=self.dest(data)
        fmt=self.ffmt("$(prod)  $(dt:amount)  $(ct:amount)  %(per)4.1f%%  $(prod_name)")
        try:
            print >>dest, self.ffmt("[[ Report by product. Period from $(date_from:param)[[ to $(date_to:param)]]") % self.opt
            if prod_group:
                print >>dest, self.ffmt("[[ Product group: $(group:param)]]") % {'group':prod_group}
            print >>dest, spln
            tot=sum([dd['ct'] for dd in data])
            for dd in data:
                dt+=dd['dt']
                ct+=dd['ct']
                dd['per']=dd['ct']*100/tot
                prod=self.prodd.get(prod_group+dd['prod'],None)
                dd['prod_name']=prod and prod['name'] or "code not in list"
                print >>dest, fmt % dd
            print >>dest, spln
            print >>dest, self.ffmt("[[Totals:]]     %8.2f  %8.2f") % (dt,ct)
        except IOError:
            pass
               
    def _rep_acc(self,args):
        rb, dt,ct = 0.0, 0.0, 0.0
        dfrom,dto=self.opt['date_from'], self.opt['date_to']
        data={}
        for r in self.select():
            acc,amount,dc=r['acc'],r['amount'],r['dc']
            rr=data.get(acc,None)
            if not rr:
                rr={'acc':acc,'rest':0.0, 'dt':0.0, 'ct':0.0}
                data[acc]=rr
            if r['date']<dfrom:
                rr['rest']+=amount*(2*dc-1)
            elif r['date']<=dto:
                rr['dt']+=amount*dc
                rr['ct']+=amount*(1-dc)
        data=data.values()
        data.sort(key=lambda r:r['acc'])
        dest=self.dest(data)
        try:
            print >>dest, self.ffmt("[[Report by accounts. Period from $(date_from:param) [[to $(date_to:param)]]") % self.opt
            print >>dest, spln
            fmt=self.ffmt("$(acc:acc_id)  $(rest:amount)  $(dt:amount)  $(ct:amount)  $(re:amount)")
            for dd in data:
                dd['re']=dd['rest']+dd['dt']-dd['ct']
                rb+=dd['rest']
                dt+=dd['dt']
                ct+=dd['ct']
                print >>dest, fmt % dd
            print >>dest, spln
            print >>dest, self.ffmt("[[Totals:]]     %8.2f  %8.2f  %8.2f  %8.2f") % (rb,dt,ct,rb+dt-ct)
        except IOError:
            pass


    def _prod_ls(self,args):
        prod_group=args and args[0] or ''
        data=[r.copy() for r in self.prodd.itervalues() if r['grp']==prod_group]
        dest=self.dest(data)
        fmt=self.ffmt("$(prod)  %(dc)d  $(name:prod_name)")
        try:
            print >>dest, self.ffmt("[[Product list. Group: $(group:param)]]") % {'group':prod_group}
            print >>dest, spln
            for r in data:
                r['prod']=r['prod'][len(prod_group):]
                print >>dest, fmt % r
        except IOError:
            pass
    def saveProds(self):
        f=open(self.db+'prod.tmp','w')
        for prod in self.prodd.itervalues():
            f.write(self.prodfmt % prod)
        f.close()
        os.remove(self.db+'prod')
        os.rename(self.db+'prod.tmp', self.db+'prod')

    def _prod_add(self,args):
        usage="Usage: prod add [-d] <prod_code> <prod_name>"
        ops,args = getopt(args,'d')
        check(len(args)==2, usage)
        ops=dict(ops)
        if '-d' in ops:
            dc=1
        else:
            dc=0
        prod_code=args[0]
        group=prod_code.rstrip('.')
        pp=group.rfind('.')
        if pp==-1:
            group=''
        else:
            group=group[:pp+1]
        chk=self.prodd.get(group,None)
        check(chk or (group==''), "Product group %s don't exists" % group)
        args={'prod':prod_code,'grp':group,'name':args[1],'dc':dc}
        self.prodd[prod_code]=args
        f=open(self.db+'prod','a')
        rr=self.prodfmt % args
        f.write(rr)
        f.close()
        print "Product %s added" % args['prod']
    def _prod_dump(self,args):
        sql="""select prod_code,prod_name,dt from prod order by prod_code"""
        for r in self.select_d(sql):
            r['dtopt']=r['dt']==1 and '-d' or ''
            print "prod add %(dtopt)s %(prod_code)s '%(prod_name)s'" % r
    def _prod_rm(self,args):
        usage="prod del <prod_code>"
        check(len(args)==1, usage)
        data=[prod for prod in self.prodd.iterkeys() if prod.startswith(args[0])]
        for prod in data:
            del self.prodd[prod]
            print "Product %s deleted." % prod
        self.saveProds()
    def _op_ls(self,args):
        fmt=self.ffmt('$(date)  $(acc:acc_id)  $(amount)  $(prod:prod_code_full)  $(note)')
        prod_code=args and args[0] or ''
        dt, ct = 0.0, 0.0
        dfrom,dto=self.opt['date_from'], self.opt['date_to']
        data=[r for r in self.select() 
                if dfrom<=r['date']<=dto and r['prod'].startswith(prod_code)]
        data.sort(key=lambda x:[x[f] for f in ('date','acc')])
        dest=self.dest(data)
        try:
            print >>dest, self.ffmt("[[Operations]]")
            print >>dest, spln
            for d in data:
                amount=d['amount']
                fdt=d['dc']
                dt+=amount*fdt
                ct+=amount*(1-fdt)
                print >>dest, fmt % d
            print >>dest, spln
            print >>dest, self.ffmt("[[Totals: dt=]]%.2f  [[ct=]]%.2f") %(dt,ct)
        except IOError:
            pass
    def _op_dump(self,args):
        sql="""select op_date,acc_id,prod_code,amount,note
            from op
            where op_date between '%(date_from)s' and '%(date_to)s'
            """ % self.opt
        for r in self.select_d(sql):
            print "op add -d %(op_date)s -a %(acc_id)s %(prod_code)s %(amount).2f '%(note)s'" % r
    def _op_add(self,args):
        usage="Usage: op add [-d <op_date>] [-a acc] <prod_code> <amount> [note]"
        ops,args = getopt(args,'a:d:')
        check(len(args)>1,usage)
        ops=dict(ops)
        prod=self.prodd.get(args[0],None)
        check(prod,'No such product code: %s' % args[0])
        if len(args)>2:
            note=args[2]
        else:
            note=''
        dd={
            'acc': ops.get('-a',self.opt['acc']),
            'date': ops.get('-d',self.opt['date']),
            'prod': prod['prod'],
            'amount': float(args[1]),
            'dc':prod['dc'],
            'note': note}
        f=open(self.db+'op','a')
        f.write(self.opfmt % dd)
        f.close()
    def do_show(self,c):
        usage="Usage: show [<option_name>]"
        cl=c.split()
        opts=self.opt.keys()
        if cl:
            opts=cl
        for o in opts:
            if o[0]!='_':
                if o in self.opt:
                    print "%s = %s" % (o, self.opt[o])
                else:
                    print "Unknown option:%s" % o
    def help_show(self):
        print """ Usage: show [<option>]
        Show current option settings (all or for <option> only)
        """
    def do_set(self,c):
        usage="Usage: set <option> <value>"
        cl=shlex.split(c)
        if len(cl)==2:
            # set option value
            if cl[0] in self.opt:
                self.opt[cl[0]]=cl[1]
            else:
                print "Unknown option: %s" % cl[0]
        else:
            print usage
        self.setPrompt()
    def help_set(self):
        print """ Usage: set <option> <value>
    Possible options:
        date - default date for new operation.
        date_from, date_to - period for all reports and operation list (op ls).
        acc - default account for new operation.
        max_lines - use console viewer (less) if report output exceed this number of lines.
    Note:
        Enter all dates in YYYY-MM-DD format.
        """

    def do_quit(self,c):
        return 1
    do_q=do_quit
    def help_quit(self):
        print "Quit the program."

def run():
    app=App()
    app.interactive=sys.stdin.isatty()
    if app.interactive:
        readline.parse_and_bind("tab: complete")
        readline.set_completer_delims(' .')
        readline.set_completer(app.complete)
        app.cmdloop()
    else:
        for ln in sys.stdin:
            app.onecmd(ln)

if __name__=='__main__':
    run()
